Micron Document
🎖️GitЯра🎖️

Commit 034005b4fa5c8b34761c4ed5d85f0d3ebd4345d8


Parents : bf5f86d
Author : Garth Vander Houwen <garthvh@yahoo.com>
Signature : Signature validation error
Date : 2026-07-05T10:28:27-07:00
Committer : GitHub <noreply@github.com>
Date : 2026-07-05T17:28:27Z

Style GeoJSON overlays from simplestyle-spec (fill/stroke) (#6088)

Co-authored-by: James Rich <james.a.rich@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Changes

1 files changed, 89 insertions(+), 1 deletions(-)


Diff

diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
index 7dad8d2fb9..7eafc16372 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
@@ -90,7 +90,10 @@ import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberUpdatedMarkerState
import com.google.maps.android.compose.widgets.ScaleBar
import com.google.maps.android.data.Layer
+import com.google.maps.android.data.geojson.GeoJsonFeature
import com.google.maps.android.data.geojson.GeoJsonLayer
+import com.google.maps.android.data.geojson.GeoJsonLineStringStyle
+import com.google.maps.android.data.geojson.GeoJsonPolygonStyle
import com.google.maps.android.data.kml.KmlLayer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -154,6 +157,8 @@ import org.meshtastic.proto.Position
import org.meshtastic.proto.Waypoint
import kotlin.math.abs
import kotlin.math.max
+import kotlin.math.roundToInt
+import android.graphics.Color as AndroidColor
// region --- Map Mode ---
@@ -192,6 +197,11 @@ private val GEOFENCE_OVERLAY_COLOR = Color(0xFFFF9800)
private const val GEOFENCE_FILL_ALPHA = 0.12f
private const val GEOFENCE_STROKE_WIDTH = 2f
+// simplestyle-spec fallbacks for imported GeoJSON overlays that omit these keys; tune here.
+// 0.35 lets stacked contour bands read as a gradient.
+private const val DEFAULT_GEOJSON_FILL_OPACITY = 0.35f
+private const val DEFAULT_GEOJSON_STROKE_WIDTH = 2f
+
// Minimum lat/lon delta (~11 m) between the two box-authoring corner taps; below this the box would be degenerate
// (zero-area) so the second tap is ignored.
private const val BOX_AUTHORING_MIN_CORNER_DELTA = 1e-4
@@ -1207,7 +1217,9 @@ private fun MapLayerOverlay(layerItem: MapLayerItem, mapViewModel: MapViewModel)
LayerType.KML -> KmlLayer(map, inputStream, context)
LayerType.GEOJSON ->
- GeoJsonLayer(map, JSONObject(inputStream.bufferedReader().use { it.readText() }))
+ GeoJsonLayer(map, JSONObject(inputStream.bufferedReader().use { it.readText() })).also {
+ it.applySimpleStyleSpec()
+ }
}
} catch (e: Exception) {
Logger.withTag("MapView").e(e) { "Error loading map layer: ${layerItem.name}" }
@@ -1248,6 +1260,82 @@ private fun Layer.safeAddLayerToMap() {
}
}
+/**
+ * Apply simplestyle-spec (https://github.com/mapbox/simplestyle-spec) properties to a GeoJSON layer.
+ *
+ * Google's [GeoJsonLayer] otherwise applies one default style to every feature, so exports that carry per-feature
+ * colors render unstyled. In particular, Meshtastic Site Planner coverage contours set `fill`/`stroke` (plus a legacy
+ * `color`) and `fill-opacity`; read those and style each polygon/line so the coverage draws in its dBm colors instead
+ * of the default black outline.
+ */
+private fun GeoJsonLayer.applySimpleStyleSpec() {
+ for (feature in features) {
+ val fill = feature.cssColor("fill") ?: feature.cssColor("color")
+ val stroke = feature.cssColor("stroke") ?: feature.cssColor("color")
+ val fillOpacity = feature.getProperty("fill-opacity")?.toFloatOrNull()
+ val strokeWidth = feature.getProperty("stroke-width")?.toFloatOrNull() ?: DEFAULT_GEOJSON_STROKE_WIDTH
+ when (feature.geometry?.geometryType) {
+ "Polygon",
+ "MultiPolygon",
+ ->
+ feature.polygonStyle =
+ GeoJsonPolygonStyle().apply {
+ fill?.let { fillColor = it.resolveFillAlpha(fillOpacity) }
+ stroke?.let { strokeColor = it }
+ this.strokeWidth = strokeWidth
+ }
+
+ "LineString",
+ "MultiLineString",
+ ->
+ feature.lineStringStyle =
+ GeoJsonLineStringStyle().apply {
+ stroke?.let { color = it }
+ width = strokeWidth
+ }
+
+ else -> Unit // Points keep the default marker.
+ }
+ }
+}
+
+private fun GeoJsonFeature.cssColor(key: String): Int? = getProperty(key)?.let { parseCssColor(it) }
+
+/**
+ * Resolve a polygon fill's alpha: `fill-opacity` wins when present; otherwise keep any alpha the color already carries
+ * (`rgba()`/`#AARRGGBB`), falling back to [DEFAULT_GEOJSON_FILL_OPACITY] for opaque fills.
+ */
+private fun Int.resolveFillAlpha(fillOpacity: Float?): Int = when {
+ fillOpacity != null -> withAlpha(fillOpacity)
+ AndroidColor.alpha(this) < 255 -> this
+ else -> withAlpha(DEFAULT_GEOJSON_FILL_OPACITY)
+}
+
+/** Parse a hex (`#RRGGBB`/`#AARRGGBB`), `rgb()/rgba()`, or named color to an ARGB int; null if invalid. */
+private fun parseCssColor(raw: String): Int? {
+ val value = raw.trim()
+ return try {
+ if (value.startsWith("rgb", ignoreCase = true)) {
+ val parts = value.substringAfter('(').substringBefore(')').split(',').map { it.trim() }
+ if (parts.size < 3) return null
+ val alpha = if (parts.size >= 4) (parts[3].toFloat() * 255f).roundToInt() else 255
+ AndroidColor.argb(alpha, parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
+ } else {
+ AndroidColor.parseColor(value) // #hex or named color
+ }
+ } catch (e: IllegalArgumentException) {
+ Logger.withTag("MapView").w(e) { "Unparseable GeoJSON color: $raw" }
+ null
+ }
+}
+
+private fun Int.withAlpha(opacity: Float): Int = AndroidColor.argb(
+ (opacity.coerceIn(0f, 1f) * 255f).roundToInt(),
+ AndroidColor.red(this),
+ AndroidColor.green(this),
+ AndroidColor.blue(this),
+)
+
// endregion
// region --- Utilities ---

Served by rngit 1.5.0 - Generated in 0.06s